/*
 * Copyright (c) 2009 Robert Esser
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.e2ser.component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;

import net.e2ser.petri.Editor;
import net.e2ser.petri.SimulatorMode;
import net.e2ser.util.XMLWriter;
import android.graphics.Rect;

/**
 * 
 * <p>
 * Title: Petri Net Editor/Simulator
 * </p>
 * <p>
 * Description: A Rob Component represents a hierarchical node
 * </p>
 * <p>
 * Copyright: Copyright (c) 2009
 * </p>
 * <p>
 * Company:
 * </p>
 * 
 * @author Rob Esser
 * @version 1.0
 */
abstract public class RobComponent extends AbstractComponent {
  
  public RobComponent(RobComponent parent, int x, int y) {
    super(parent, x, y);
  }

  private ArrayList<AbstractComponent> subNodes = new ArrayList<AbstractComponent>();
  private ArrayList<OneD> arcs = new ArrayList<OneD>();
  private Random random = new Random();
  //caching neighbours improves editing and simulation
  private transient HashMap<AbstractComponent, ArrayList<AbstractComponent>> neighbourCache;
  private transient HashMap<AbstractComponent, ArrayList<AbstractComponent>> inputsCache;
  private transient HashMap<AbstractComponent, ArrayList<AbstractComponent>> outputsCache;
  private transient HashMap<AbstractComponent, ArrayList<AbstractComponent>> onlyOutputsCache;

  /**
   * Add any fireable subnodes to the firelist
   * 
   * @param fireList
   *          List
   * @param mode
   *          SimulatorMode
   */
  public void addFireable(ArrayList<AbstractComponent> fireList, SimulatorMode mode) {
    int num = subNodes.size();
    for (int i = 0; i < num; i++) {
      (subNodes.get(i)).addFireable(fireList, mode);
    }
  }

  /**
   * simulate a single step by checking my subnodes for components that cam
   * simulate now
   * 
   * @param mode
   *          SimulatorMode
   * @param animation
   *          boolean
   * @param delay
   *          int
   * @return int
   */
  public int step(SimulatorMode mode, boolean animation, int delay) {
    ArrayList<AbstractComponent> fireList = new ArrayList<AbstractComponent>();
    this.addFireable(fireList, mode);
    if (fireList.size() == 0) {
      return 0;
    } else {
      AbstractComponent ele = fireList.get(Math.abs(random.nextInt(fireList.size())));
      ele.fire(mode, animation, delay);
      return 1;
    }
  }

  /**
   * return the number of sub nodes
   * 
   * @return int
   */
  public int size() {
    return this.subNodes.size();
  }

  /**
   * return the number of connectors
   * 
   * @return int
   */
  public int connectorsSize() {
    return this.arcs.size();
  }

  /**
   * return the subnode at index i
   * 
   * @param i
   *          int
   * @return AbstractComponent
   */
  public AbstractComponent elementAt(int i) {
    return subNodes.get(i);
  }

  /**
   * return the subnode with name
   * 
   * @param str
   *          String
   * @return AbstractComponent
   */
  public AbstractComponent subnodeWithName(String str) {
    for (int i = 0; i < size(); i++) {
      AbstractComponent ele = subNodes.get(i);
      if (ele.name.equals(str)) {
        return ele;
      }
    }
    return null;
  }

  /**
   * return connection at i
   * 
   * @param i
   *          int
   * @return OneD
   */
  public OneD connectionElementAt(int i) {
    return arcs.get(i);
  }

  /**
   * insert the element into the current hierarchy
   * 
   * @param node
   *          AbstractComponent
   * @param x
   *          int
   * @param y
   *          int
   */
  public void insertElementAt(AbstractComponent node, int x, int y) {
    node.move(x, y);
    subNodes.add(node);
  }

  /**
   * insert the element into the current hierarchy
   * 
   * @param node
   *          AbstractComponent
   */
  public void insertElement(AbstractComponent node) {
    subNodes.add(node);
  }

  /**
   * AbstractComponent:insertElementIn This should never be called at the moment
   * 
   * @param node
   *          RobComponent
   * @param x
   *          int
   * @param y
   *          int
   * @return AbstractComponent
   */
  public AbstractComponent insertElementIn(RobComponent node, int x, int y, double scale) {
    return null;
  }

  /**
   * delete the elements of selection
   * 
   * @param selection
   *          Vector
   */
  public void deleteElements(ArrayList<AbstractComponent> selection) {
    int num = selection.size();
    for (int i = 0; i < num; i++) {
      deleteElement(selection.get(i));
    }
  }

  /**
   * delete the component and any attached arcs
   * 
   * @param comp
   *          AbstractComponent
   */
  public void deleteElement(AbstractComponent comp) {
    int connNum = connectorsSize();
    ArrayList<OneD> delConn = new ArrayList<OneD>();
    for (int j = 0; j < connNum; j++) {
      OneD a = connectionElementAt(j);
      if (a.from.equals(comp) | a.to.equals(comp)) {
        delConn.add(a);
      }
    }
    // now delete the connectors
    connNum = delConn.size();
    for (int j = 0; j < connNum; j++) {
      arcs.remove(delConn.get(j));
      invalidateCaches(delConn.get(j));
    }
    // delete element
    subNodes.remove(comp);
    invalidateCaches(comp);
  }

  /**
   * return all the elements that are inputs to ele
   * 
   * @param ele
   *          AbstractComponent
   * @return List
   */
  public ArrayList<AbstractComponent> allInputs(AbstractComponent ele) {
    ArrayList<AbstractComponent> ins = null;
    if (inputsCache == null) {
      inputsCache = new HashMap<AbstractComponent, ArrayList<AbstractComponent>>();
    }
    ins = inputsCache.get(ele);
    if (ins == null) {
      //need to create cache entry
      ins = new ArrayList<AbstractComponent>();
      int num = connectorsSize();
      OneD a = null;
      for (int i = 0; i < num; i++) {
        a = connectionElementAt(i);
        if (a.to.equals(ele)) {
          ins.add(a.from);
        }
      }
      inputsCache.put(ele, ins);
    }
    return ins;
  }

  /**
   * return all the elements that are outputs to ele
   * 
   * @param ele
   *          AbstractComponent
   * @return List
   */
  public ArrayList<AbstractComponent> allOutputs(AbstractComponent ele) {
    ArrayList<AbstractComponent> outs = null;
    if (outputsCache == null) {
      outputsCache = new HashMap<AbstractComponent, ArrayList<AbstractComponent>>();
    }
    outs = outputsCache.get(ele);
    if (outs == null) {
      //need to create cache entry
      outs = new ArrayList<AbstractComponent>();
      int num = connectorsSize();
      OneD a = null;
      for (int i = 0; i < num; i++) {
        a = connectionElementAt(i);
        if (a.from.equals(ele)) {
          outs.add(a.to);
        }
      }
      outputsCache.put(ele, outs);
    }
    return outs;
  }

  /**
   * return all the elements that are only outputs to ele
   * 
   * @param ele
   *          AbstractComponent
   * @param ins
   *          Vector
   * @return Vector
   */
  public ArrayList<AbstractComponent> onlyOutputs(AbstractComponent ele, ArrayList<AbstractComponent> ins) {
    ArrayList<AbstractComponent> outs = null;
    if (onlyOutputsCache == null) {
      onlyOutputsCache = new HashMap<AbstractComponent, ArrayList<AbstractComponent>>();
    }
    outs = onlyOutputsCache.get(ele);
    if (outs == null) {
      //need to create cache entry
      outs = new ArrayList<AbstractComponent>();
      int num = connectorsSize();
      OneD a = null;
      for (int i = 0; i < num; i++) {
        a = connectionElementAt(i);
        if (a.from.equals(ele) && (!ins.contains(a.to))) {
          outs.add(a.to);
        }
      }
      onlyOutputsCache.put(ele, outs);
    }
    return outs;
  }

  /**
   * return all the elements that are only outputs to ele
   * 
   * @param ele
   *          AbstractComponent
   * @return List
   */
  public ArrayList<AbstractComponent> onlyOutputs(AbstractComponent ele) {
    ArrayList<AbstractComponent> ins = allInputs(ele);
    return onlyOutputs(ele, ins);
  }

  /**
   * insert or delete a connection check to see if the connection already exists
   * 
   * @param from AbstractComponent
   * @param to AbstractComponent
   */
  public void insertDeleteArc(AbstractComponent from, AbstractComponent to) {
    boolean exists = false;
    int num = connectorsSize();
    OneD a = null;
    for (int i = 0; i < num; i++) {
      if (exists) {
        break;
      }
      a = connectionElementAt(i);
      exists = a.from.equals(from) && a.to.equals(to);
    }
    if (exists) {
      arcs.remove(a);
    } else {
      // insert if connection does not exist
      a = new Arc(from, to);
      arcs.add(a);
      a.resize(Editor.DisplayScale);
    }
    invalidateCaches(a);
  }

  private void invalidateCaches(OneD arc) {
    assert arc != null;
    if (neighbourCache != null) {
      //invalidate cache entries
      neighbourCache.remove(arc.from);
      neighbourCache.remove(arc.to);
    }
    if (inputsCache != null) {
      //invalidate cache entries
      inputsCache.remove(arc.to);
    }
    if (outputsCache != null) {
      //invalidate cache entries
      outputsCache.remove(arc.from);
    }
    if (onlyOutputsCache != null) {
      //invalidate cache entries
      onlyOutputsCache.remove(arc.from);
    }
  }
  
  private void invalidateCaches(AbstractComponent comp) {
    if (neighbourCache != null) {
      //invalidate cache entry
      neighbourCache.remove(comp);
    }
    if (inputsCache != null) {
      //invalidate cache entry
      inputsCache.remove(comp);
    }
    if (outputsCache != null) {
      //invalidate cache entry
      outputsCache.remove(comp);
    }
    if (onlyOutputsCache != null) {
      //invalidate cache entry
      onlyOutputsCache.remove(comp);
    }
  }

  public Rect neighboursBoundsOf(AbstractComponent node) {
    ArrayList<AbstractComponent> neighbours = null;
    if (neighbourCache == null) {
      neighbourCache = new HashMap<AbstractComponent, ArrayList<AbstractComponent>>();
    }
    neighbours = neighbourCache.get(node);
    if (neighbours == null) {
      neighbours = new ArrayList<AbstractComponent>();
      neighbours.addAll(allInputs(node));
      neighbours.addAll(allOutputs(node));
      neighbourCache.put(node, neighbours);
    }
    Rect r = new Rect(node.bounds());
    for (int i = 0; i < neighbours.size(); i++) {
      r.union(neighbours.get(i).bounds());
    }
    return r;
  }

  /**
   * Output an XML representation of myself
   * @param writer object that represents writer state
   * @throws IOException 
   */
  public void toXML(XMLWriter writer) throws IOException {
    String id = writer.addElement(this);
    
    writer.writeLine("<top id=\"" + id + "\">");
    writer.indent();
    //first write node references
    writer.writeLine("<nodes>");
    writer.indent();
    for (int i = 0; i < subNodes.size(); i++) {
      String snid = writer.addElement(subNodes.get(i));
      writer.writeLine("<ref id=\"" + snid + "\"/>");
    }
    writer.detent();
    writer.writeLine("</nodes>");
    writer.writeLine("<arcs>");
    writer.indent();
    //now write connections
    for (int i = 0; i < arcs.size(); i++) {
      String cid = writer.addElement(arcs.get(i));
      writer.writeLine("<ref id=\"" + cid + "\"/>");
    }
    writer.detent();
    writer.writeLine("</arcs>");
    //now write nodes
    for (int i = 0; i < subNodes.size(); i++) {
      subNodes.get(i).toXML(writer);
    }
    //now write connections
    for (int i = 0; i < arcs.size(); i++) {
      arcs.get(i).toXML(writer);
    }
    writer.detent();
    writer.writeLine("</top>\n");
  }



}
